package com.atguigu.gulimall.authserver.controller;

import com.alibaba.fastjson.TypeReference;
import com.atguigu.common.constant.AuthServerConstant;
import com.atguigu.common.exception.BizCodeEnum;
import com.atguigu.common.utils.R;
import com.atguigu.common.vo.MemberRespVo;
import com.atguigu.gulimall.authserver.feign.MemberFeignService;
import com.atguigu.gulimall.authserver.feign.ThirdPartFeignService;
import com.atguigu.gulimall.authserver.vo.UserLoginVo;
import com.atguigu.gulimall.authserver.vo.UserRegistVo;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.stereotype.Controller;
import org.springframework.util.StringUtils;
import org.springframework.validation.BindingResult;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;

import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import javax.validation.Valid;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;

@Controller
@Slf4j
public class LoginController {

    @Resource
    private MemberFeignService memberFeignService;   //用于调用gulimall-member服务执行真正的用户信息的注册

    @Resource
    private ThirdPartFeignService thirdPartFeignService;

    @Resource
    private StringRedisTemplate stringRedisTemplate;

    /**
     * 发送一个请求直接跳转到一个页面，我们不想写一大堆空实现，只想跳转到一个页面
     *
     * SpringMvc 的viewcontroller可以帮助我们将请求和页面映射过来
     * @return
     */
        //    @GetMapping("/login.html")
//    public String loginPage(){
//
//        return "login";
//    }
//
//    @GetMapping("/reg.html")
//    public String regPage(){
//
//        return "reg";
//    }

    /**
     * 调用第三方接口获取验证码功能
     *
     * 对于调用第三方服务发送验证码给用户需要注意以下两点：
     * 1）用户不能刚发送完验证码请求就立马刷新页面进行第二次请求（接口防刷）
     * 2）用户获得的验证码应该设置有效期，比如1分钟之内有效  （设置过期时间并与用户收到的验证码再次校验）
     *
     * 对于2，我们一般使用redis将验证码存储起来，因为验证码不需要持久化策略
     * @param phone
     * @return
     */
    @ResponseBody
    @RequestMapping("/sms/sendcode")
    public R sendCode(@RequestParam("phone") String phone){
        String code = UUID.randomUUID().toString().substring(0,5);
        ValueOperations<String, String> operations = stringRedisTemplate.opsForValue();
        //TODO 1.接口防刷。下面的代码还并没有实现
        //(在60s内不能再次发送验证码)给验证码生成一个系统时间，如果用户使用同一个手机号重复请求，则先会来到redis中查出是否超出了10
        //分钟。可能你会问，我非要去redis中查是为什么呢，我不去调用第三方接口得了呗，就是直接阻止用户请求。这样可能会
        //出问题，第三方接口可能不能保证正常运行，可能在后端生成了随机验证码，但不一定百分百发送给了客户，客户重复
        //刷接口可能并不一定处于恶意，他真有可能没收到验证码。但由于你想添加验证码校验功能，将这段随机数存入redis
        //中后就把问题变复杂了，即验证码生成并存起来了，但用户没收到。我们需要做的就是进行一次判断
        String redisCode = operations.get(AuthServerConstant.SMS_CODE_PREFIX + phone);
        if(!StringUtils.isEmpty(redisCode)){
            Long startTime = Long.parseLong(redisCode.split("_")[1]);
            long currentTime = System.currentTimeMillis();
            //在60s内不能再次发送验证码
            if((currentTime - startTime) <60000){
                return R.error(BizCodeEnum.SMS_CODE_EXCEPTION.getCode(),BizCodeEnum.SMS_CODE_EXCEPTION.getMsg());
            }
        }

        //2.验证码的再次校验。我们使用redis，key就是手机号，value就是随机生成的code值
        //例如：key是 sms:code:18800461178   value是 wea89
        //这里我们已经在com.atguigu.common.constant.AuthServerConstant下面写好了key
        // 的前缀sms:code
        String s = code + "_"+System.currentTimeMillis();
        //将验证码存入redis中并指定过期时间
        operations.set(AuthServerConstant.SMS_CODE_PREFIX+phone,s,10, TimeUnit.MINUTES);
        thirdPartFeignService.sendCode(phone,code);
        return R.ok();
    }


    /**
     * 由于我们使用转发会出现表单重复提交(刷新就会出现)，那么为了避免这种情况，
     * 我们让注册失败的页面直接重定向回注册页面，但如果想将
     * 原来的注册页面的失败信息带到新的注册页面，model就实现不了(model只能放到请求域中，重定向页面获取不到)，我们需要
     * 新的参数：RedirectAttributes,该参数可以模拟重定向携带数据
     *
     * TODO 重定向携带数据的原理是利用了session，将数据存放在session中
     * 只要跳到下一个页面取出这个数据后，session里面的数据就会删掉
     *
     * TODO 我们未来会遇到分布式情况下的session问题
     *
     * 下面是我总结的一个小坑：当提到表单提交注册信息时，你有没有想到POST提交需要用到@RequestBody注解呢？
     * 不错，在我们之前的后台系统对信息的增删该操作时用的POST请求都是使用了该注解，但在这里就不行。如果你上网查的话
     * 会发现对于POST提交方式来讲，其对应的Content-Type常见有四种，对于ajax请求来讲，它的Content-Type是application/json，
     * 也就是前后端交互的时候将数据拆解为json字符串进行传输，因为兼容效果好而广为流传，因此在这种情况下对于POST请求参数必须用
     *  @RequestBody 进行修饰。而对于表单提交来讲，它POST提交的Content-Type是application/x-www-form-urlencoded，只会将
     *  数据以key=value、每队之间用&进行分割的方式进行提交，并不是json格式，这时候我们就不需要加其他注解来修饰参数了
     *
     *  总的来讲，只有以json格式进行传输并且是POST提交的时候，才需要@RequestBody注解
     *
     * @param userRegistVo
     * @param bindingResult
     * @param redirectAttributes
     * @return
     */
    @PostMapping("/regist")
    public String regist(@Valid UserRegistVo userRegistVo, BindingResult bindingResult, RedirectAttributes redirectAttributes){
        if(bindingResult.hasErrors()){
            //校验出错，转发到注册页面
            Map<String, String> errors = bindingResult.getFieldErrors().stream().collect(Collectors.toMap(FieldError::getField,FieldError::getDefaultMessage,(v1,v2) -> v2));
            redirectAttributes.addFlashAttribute("errors",errors);
//            model.addAttribute("errors",errors);


//            return "forward:/reg.html";  这样做会出现问题：表单POST提交被/regist拦截，接着进行转发，通过配置的
            //路径映射转发回注册页面，但默认配置的视图控制器是GET请求方式，我们相当于外面是一个POST提交，但里面在进行
            //GET请求，这样的POST提交是不被允许的，我们只能回到之前的做法，返回可以被MVC渲染的页面的名字
            return "redirect:http://auth.gulimall.com/reg.html";
        }
        //真正的注册，调用远程服务进行注册

        //1.校验验证码
        String code = userRegistVo.getCode();
        String redisCode = stringRedisTemplate.opsForValue().get(AuthServerConstant.SMS_CODE_PREFIX + userRegistVo.getPhone());
        //先看redis中能不能取出验证码，如果不能，说明已过期。如果取出比对一致，则调用真正的注册接口，否则还是提示错误信息

        if(!StringUtils.isEmpty(redisCode)){
            if(code.equals(redisCode.split("_")[0])){
                //删除redis中的验证码，令牌机制
                stringRedisTemplate.delete(AuthServerConstant.SMS_CODE_PREFIX + userRegistVo.getPhone());
                //下面才是真正的注册，使用feign调用会员服务，gulimall-member,进行用户信息的注册
                //我们需要先写远程接口：com.atguigu.gulimall.member.controller.MemberController
                R regist = memberFeignService.regist(userRegistVo);
                if(regist.getCode() == 0){
                    return "redirect:http://auth.gulimall.com/login.html";
                }else{
                    Map<String, String> errors = new HashMap<>();
                    errors.put("msg",regist.get("msg").toString());
                    redirectAttributes.addFlashAttribute("errors",errors);
                    return "redirect:http://auth.gulimall.com/reg.html";
                }
            }else{
                Map<String, String> errors = new HashMap<>();
                errors.put("code","验证码错误");
                redirectAttributes.addFlashAttribute("errors",errors);
                return "redirect:http://auth.gulimall.com/reg.html";
            }


        }else {
            Map<String, String> errors = new HashMap<>();
            errors.put("code","验证码错误");
            redirectAttributes.addFlashAttribute("errors",errors);
            return "redirect:http://auth.gulimall.com/reg.html";
        }
        //注册成功回到首页或登录页,因为我们配置了视图控制器：com.atguigu.gulimall.authserver.config.GulimallWebConfig
        //我们只需要写上url的path路径即可(不需要写重定向全路径)，它会自动帮助我们进行映射
//        return "redirect:http://auth.gulimall.com/login.html";
//        return "redirect:/login.html";
    }


    @PostMapping("/login")
    public String login(UserLoginVo userLoginVo, RedirectAttributes redirectAttributes, HttpSession session){
        R login = memberFeignService.login(userLoginVo);
        if(login.getCode() == 0){
            MemberRespVo data = login.getData("data", new TypeReference<MemberRespVo>() {
            });
            System.err.println("用户信息："+data);
            log.info("登陆成功，用户信息：{}",data);
            //第一次使用session，命令浏览器保存卡号，即JSESSIONID这个cookie
            //以后浏览器访问哪个网站就需要这个网站的cookie
            //为了解决session不能跨域的问题，现在面临的问题就是子域内部是可以访问，但父域访问不了
            //着我们是可以通过代码解决的(你会看到为什么谷粒商城整个项目的不同服务名被设计成这样，妙啊)

            //TODO 1.默认发的令牌的作用域只是当前域(解决子域session共享的问题)
            //TODO 2.我们想使用JSON序列化的方式来序列化对象数据到redis中
            session.setAttribute(AuthServerConstant.LOGIN_USER,data);
//            MemberRespVo loginUser = (MemberRespVo)session.getAttribute("loginUser");
//            httpServletResponse.addCookie(new Cookie(""));    我们不这么做，SpringSession为我们完成了分布式session的解决方案
            return "redirect:http://gulimall.com";
        }else{
            Map<String,String> errors = new HashMap<>();
            errors.put("msg",login.get("msg").toString());
            redirectAttributes.addFlashAttribute("errors",errors);
            return "redirect:http://auth.gulimall.com/login.html";
        }

    }

    /**
     * 对登录跳转逻辑的完善：如果是已经登录的用户，他即使输入http://auth.gulimall.com/login.html
     * 也不能让他再跳转到登录页，而是必须回到首页
     * @return
     */
    @GetMapping("/login.html")
    public String loginPage(HttpSession session){
        Object attribute = session.getAttribute(AuthServerConstant.LOGIN_USER);
        if(attribute == null){
            return "login";
        }else {
            return "redirect:http://gulimall.com";
        }

    }
}
